開發 UI 畫面的過程中,常見的功能開發情境有很多,除了顯示資訊以外,另外一個常見的情境是提供一個按鈕,讓使用者執行特定功能了。在 Flutter 中,如果要製作一個 UI 讓使用者可以點擊,我們通常會使用 GestureDetector 並在其 onTap 參數中給定執行方法。
在經歷過前幾天討論的主題,有時候我們會發現,經過不斷地重構,最終按鈕事件是在子 Widget 中綁定。在下面的例子中,GameItemView 是列表中的每一個 Item,而當中包含一個購買按鈕。為了讓按鈕能正常工作,我們把 buyGame 方法當作參數,一路向子 Widget 傳,直到購買按鈕裡頭的 GestureDetetor。
https://dartpad.dev/?id=a78b57b5a2225b2344376eddd537a86b
![it_img_4_2_1.png](
這種做法十分簡單,也十分易懂。但是隨著 Widget 越抽越多層,Callback 方法需要透過參數,一路往子層傳遞,使用起來就不是這麼方便。而且中間層的 GameItemView,自己不需要這個方法,卻被迫開出這個 onTap 參數,只為了把他傳遞給 GamePurchaseButton。
為了解決這個問題,讓我們來嘗試另外一個作法,我們利用 BuildContext.findAncestorWidgetOfExactType() 來取得 GameListScreen,並直接執行其身上的 buyGame 方法。
https://dartpad.dev/?id=f40b2e09b5a5756236c3627ff96464a3
透過這個方式,我們可以消除掉中間 Widget 身上的 onTap。當使用者按下按鈕,嘗試執行方法時,子 Widget 就會透過 BuildContext 尋找目標 Widget,然後呼叫其身上的方法。
這個方式十分方便,消除了中間 Widget 的多餘參數,但是這個做法也帶來了比較明顯的缺點:子 Widget 直接依賴了目標 Widget,造成子 Widget 比較難以被重複使用。以上面的例子來說,假設今天有一個 Other 也同樣使用了 GameItemView,當 GamePurchaseButton 被按下時,由於找不到 GameListScreen,所以按鈕無法正常運作。
因為祖先 Widget 中不存在 GameListScreen,context.findAncestorWidgetOfExactType 時就會找回 null,我們也必須多檢查 null,避免不預期的錯誤。
如果執行方法的 Widget 與擁有方法的 Widget 距離並不是太遠,使用參數傳遞可能是比較合適的方式。如果真的發現參數需要傳遞太多層,或許可以思考是否 Widget 拆了太多層了,可以適時考慮合併。雖然 context.findAncestorWidgetOfExactType 可以解決 onTap 傳太多層的問題,但也讓 Widget 難以重用,如非必要,還是優先考慮傳遞參數為主,畢竟傳遞參數的一大優點就是能清晰的呈現意圖。